<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>豆包ASR语音识别测试 - v3</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .controls {
            margin-bottom: 20px;
        }
        button {
            padding: 10px 20px;
            margin: 5px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        .start-btn {
            background-color: #4CAF50;
            color: white;
        }
        .stop-btn {
            background-color: #f44336;
            color: white;
        }
        .start-btn:disabled, .stop-btn:disabled {
            background-color: #cccccc;
            cursor: not-allowed;
        }
        .status {
            padding: 10px;
            margin: 10px 0;
            border-radius: 4px;
            font-weight: bold;
        }
        .status.connected {
            background-color: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        .status.disconnected {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
        .status.recording {
            background-color: #fff3cd;
            color: #856404;
            border: 1px solid #ffeaa7;
        }
        .log {
            background-color: #f8f9fa;
            border: 1px solid #dee2e6;
            border-radius: 4px;
            padding: 15px;
            height: 300px;
            overflow-y: auto;
            font-family: monospace;
            font-size: 12px;
            white-space: pre-wrap;
        }
        .result {
            background-color: #e7f3ff;
            border: 1px solid #b3d9ff;
            border-radius: 4px;
            padding: 15px;
            margin: 10px 0;
            min-height: 50px;
        }
        .info {
            background-color: #d1ecf1;
            border: 1px solid #bee5eb;
            border-radius: 4px;
            padding: 10px;
            margin: 10px 0;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>豆包ASR语音识别测试 - v3</h1>
        
        <div class="info">
            <strong>测试说明：</strong><br>
            - 采样率：8kHz<br>
            - 位深：16bit<br>
            - 声道：单声道<br>
            - 格式：PCM原始数据<br>
            - 分包：200ms (1600字节)
        </div>

        <div class="controls">
            <button id="startBtn" class="start-btn">开始录音</button>
            <button id="stopBtn" class="stop-btn" disabled>停止录音</button>
            <button id="clearBtn">清空日志</button>
        </div>

        <div id="status" class="status disconnected">状态：未连接</div>

        <div class="result">
            <strong>识别结果：</strong>
            <div id="resultText">等待开始录音...</div>
        </div>

        <div>
            <strong>调试日志：</strong>
            <div id="log" class="log"></div>
        </div>
    </div>

    <script>
        const startBtn = document.getElementById("startBtn");
        const stopBtn = document.getElementById("stopBtn");
        const clearBtn = document.getElementById("clearBtn");
        const statusDiv = document.getElementById("status");
        const resultText = document.getElementById("resultText");
        const logDiv = document.getElementById("log");

        let audioContext;
        let processor;
        let source;
        let socket;
        let isRecording = false;
        let lastResult = ""; // 记录上一次的识别结果

        // 日志函数
        function log(message) {
            const timestamp = new Date().toLocaleTimeString();
            logDiv.textContent += `[${timestamp}] ${message}\n`;
            logDiv.scrollTop = logDiv.scrollHeight;
        }

        // 更新状态
        function updateStatus(message, className) {
            statusDiv.textContent = `状态：${message}`;
            statusDiv.className = `status ${className}`;
        }

        // 更新识别结果（避免重复显示）
        function updateResult(newText) {
            if (newText !== lastResult) {
                resultText.textContent = newText;
                lastResult = newText;
                log(`识别结果更新: ${newText}`);
            }
        }

        // 重采样：48kHz -> 8kHz
        function downsampleBuffer(buffer, inputSampleRate, outputSampleRate) {
            if (outputSampleRate === inputSampleRate) {
                return buffer;
            }
            const sampleRateRatio = inputSampleRate / outputSampleRate;
            const newLength = Math.round(buffer.length / sampleRateRatio);
            const result = new Float32Array(newLength);
            let offsetResult = 0;
            let offsetBuffer = 0;
            while (offsetResult < result.length) {
                const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
                let accum = 0, count = 0;
                for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
                    accum += buffer[i];
                    count++;
                }
                result[offsetResult] = accum / count;
                offsetResult++;
                offsetBuffer = nextOffsetBuffer;
            }
            return result;
        }

        // Float32 [-1,1] 转 Int16 [-32768,32767]
        function convertFloat32ToInt16(buffer) {
            const l = buffer.length;
            const result = new Int16Array(l);
            for (let i = 0; i < l; i++) {
                let s = Math.max(-1, Math.min(1, buffer[i]));
                result[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
            }
            return result;
        }

        // 发送心跳
        function sendHeartbeat() {
            if (socket && socket.readyState === WebSocket.OPEN) {
                socket.send(JSON.stringify({ type: "heartbeat" }));
                log("发送心跳包");
            }
        }

        // 开始录音
        startBtn.onclick = async () => {
            try {
                log("开始初始化录音...");
                lastResult = ""; // 重置上一次的识别结果
                
                // 建立WebSocket连接
                socket = new WebSocket("ws://127.0.0.1:8000/ws/speech_v3");
                
                socket.onopen = () => {
                    log("WebSocket连接已建立");
                    updateStatus("已连接", "connected");
                    
                    // 开始录音
                    startRecording();
                };

                socket.onmessage = (event) => {
                    try {
                        const data = JSON.parse(event.data);
                        log(`收到消息: ${JSON.stringify(data)}`);
                        
                        if (data.type === "pong") {
                            log("收到心跳响应");
                        } else if (data.type === "result") {
                            updateResult(data.text);
                        }
                    } catch (e) {
                        log(`消息解析失败: ${e.message}`);
                    }
                };

                socket.onclose = () => {
                    log("WebSocket连接已关闭");
                    updateStatus("连接已断开", "disconnected");
                    stopRecording();
                };

                socket.onerror = (error) => {
                    log(`WebSocket错误: ${error}`);
                    updateStatus("连接错误", "disconnected");
                };

            } catch (error) {
                log(`初始化失败: ${error.message}`);
                updateStatus("初始化失败", "disconnected");
            }
        };

        // 开始录音
        async function startRecording() {
            try {
                const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
                audioContext = new (window.AudioContext || window.webkitAudioContext)();
                const inputSampleRate = audioContext.sampleRate;
                source = audioContext.createMediaStreamSource(stream);

                processor = audioContext.createScriptProcessor(4096, 1, 1);
                let frameCount = 0;
                let buffer = new Int16Array(0);

                processor.onaudioprocess = (e) => {
                    if (!isRecording) return;
                    
                    const inputData = e.inputBuffer.getChannelData(0);
                    // 重采样到8kHz
                    const downsampledBuffer = downsampleBuffer(inputData, inputSampleRate, 8000);
                    // 转换为Int16
                    const int16Buffer = convertFloat32ToInt16(downsampledBuffer);
                    
                    // 累积数据
                    const newBuffer = new Int16Array(buffer.length + int16Buffer.length);
                    newBuffer.set(buffer);
                    newBuffer.set(int16Buffer, buffer.length);
                    buffer = newBuffer;

                    // 每1600字节（8000采样 * 2字节 * 0.1秒）发送一次
                    while (buffer.length >= 800) {
                        const chunk = buffer.slice(0, 800);
                        buffer = buffer.slice(800);
                        
                        if (socket && socket.readyState === WebSocket.OPEN) {
                            const base64Data = btoa(String.fromCharCode.apply(null, new Uint8Array(chunk.buffer)));
                            
                            // 发送起始帧
                            if (frameCount === 0) {
                                socket.send(JSON.stringify({
                                    type: "audio",
                                    frame_type: 0,
                                    data: base64Data
                                }));
                                log("发送起始帧");
                            } else {
                                socket.send(JSON.stringify({
                                    type: "audio",
                                    frame_type: 1,
                                    data: base64Data
                                }));
                            }
                            frameCount++;
                        }
                    }
                };

                source.connect(processor);
                processor.connect(audioContext.destination);

                isRecording = true;
                updateStatus("正在录音", "recording");
                startBtn.disabled = true;
                stopBtn.disabled = false;
                
                log("录音已开始");
                
                // 启动心跳
                setInterval(sendHeartbeat, 30000); // 每30秒发送一次心跳
                
            } catch (error) {
                log(`录音启动失败: ${error.message}`);
                updateStatus("录音启动失败", "disconnected");
            }
        }

        // 停止录音
        stopBtn.onclick = () => {
            stopRecording();
        };

        function stopRecording() {
            isRecording = false;
            
            if (processor) {
                processor.disconnect();
                processor = null;
            }
            if (source) {
                source.disconnect();
                source = null;
            }
            if (audioContext) {
                audioContext.close();
                audioContext = null;
            }
            
            // 发送结束帧
            if (socket && socket.readyState === WebSocket.OPEN) {
                socket.send(JSON.stringify({
                    type: "audio",
                    frame_type: 2,
                    data: ""
                }));
                log("发送结束帧");
            }
            
            if (socket) {
                socket.close();
            }
            
            startBtn.disabled = false;
            stopBtn.disabled = true;
            updateStatus("已停止", "disconnected");
            log("录音已停止");
        }

        // 清空日志
        clearBtn.onclick = () => {
            logDiv.textContent = "";
            resultText.textContent = "等待开始录音...";
        };

        // 页面卸载时清理
        window.onbeforeunload = () => {
            if (isRecording) {
                stopRecording();
            }
        };
    </script>
</body>
</html> 